/*
 * Copyright (c) 2008, Nicolas Cannasse
 * All rights reserved.
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *   - Redistributions of source code must retain the above copyright
 *     notice, this list of conditions and the following disclaimer.
 *   - Redistributions in binary form must reproduce the above copyright
 *     notice, this list of conditions and the following disclaimer in the
 *     documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE HAXE PROJECT CONTRIBUTORS "AS IS" AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE HAXE PROJECT CONTRIBUTORS BE LIABLE FOR
 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
 * DAMAGE.
 */
package hscript;
import hscript.Expr;

class Bytes {

	var bin : haxe.io.Bytes;
	var bout : haxe.io.BytesBuffer;
	var pin : Int;
	var hstrings : #if haxe3 Map<String,Int> #else Hash<Int> #end;
	var strings : Array<String>;
	var nstrings : Int;

	function new( ?bin ) {
		this.bin = bin;
		pin = 0;
		bout = new haxe.io.BytesBuffer();
		hstrings = #if haxe3 new Map() #else new Hash() #end;
		strings = [null];
		nstrings = 1;
	}

	function doEncodeString( v : String ) {
		var vid = hstrings.get(v);
		if( vid == null ) {
			if( nstrings == 256 ) {
				hstrings = #if haxe3 new Map() #else new Hash() #end;
				nstrings = 1;
			}
			hstrings.set(v,nstrings);
			bout.addByte(0);
			var vb = haxe.io.Bytes.ofString(v);
			bout.addByte(vb.length);
			bout.add(vb);
			nstrings++;
		} else
			bout.addByte(vid);
	}

	function doDecodeString() {
		var id = bin.get(pin++);
		if( id == 0 ) {
			var len = bin.get(pin);
			var str = bin.readString(pin+1,len);
			pin += len + 1;
			if( strings.length == 255 )
				strings = [null];
			strings.push(str);
			return str;
		}
		return strings[id];
	}

	function doEncodeConst( c : Const ) {
		switch( c ) {
		case CInt(v):
			if( v >= 0 && v <= 255 ) {
				bout.addByte(0);
				bout.addByte(v);
			} else {
				bout.addByte(1);
				bout.addByte(v & 0xFF);
				bout.addByte((v >> 8) & 0xFF);
				bout.addByte((v >> 16) & 0xFF);
				bout.addByte(v >>> 24);
			}
		#if !haxe3
		case CInt32(v):
			bout.addByte(4);
			var mid = haxe.Int32.toInt(haxe.Int32.and(v,haxe.Int32.ofInt(0xFFFFFF)));
			bout.addByte(mid & 0xFF);
			bout.addByte((mid >> 8) & 0xFF);
			bout.addByte(mid >> 16);
			bout.addByte(haxe.Int32.toInt(haxe.Int32.ushr(v, 24)));
		#end
		case CFloat(f):
			bout.addByte(2);
			doEncodeString(Std.string(f));
		case CString(s):
			bout.addByte(3);
			doEncodeString(s);
		}
	}

	function doDecodeConst() {
		return switch( bin.get(pin++) ) {
		case 0:
			CInt(bin.get(pin++));
		case 1:
			var i = bin.get(pin) | (bin.get(pin+1) << 8) | (bin.get(pin+2) << 16) | (bin.get(pin+3) << 24);
			pin += 4;
			CInt(i);
		case 2:
			CFloat( Std.parseFloat(doDecodeString()) );
		case 3:
			CString( doDecodeString() );
		#if !haxe3
		case 4:
			var i = bin.get(pin) | (bin.get(pin+1) << 8) | (bin.get(pin+2) << 16);
			var j = bin.get(pin+3);
			pin += 4;
			CInt32(haxe.Int32.or(haxe.Int32.ofInt(i), haxe.Int32.shl(haxe.Int32.ofInt(j), 24)));
		#end
		default:
			throw "Invalid code "+bin.get(pin-1);
		}
	}

	function doEncode( e : Expr ) {
		bout.addByte(Type.enumIndex(e));
		switch( e ) {
		case EConst(c):
			doEncodeConst(c);
		case EIdent(v):
			doEncodeString(v);
		case EVar(n,_,e):
			doEncodeString(n);
			if( e == null )
				bout.addByte(255);
			else
				doEncode(e);
		case EParent(e):
			doEncode(e);
		case EBlock(el):
			bout.addByte(el.length);
			for( e in el )
				doEncode(e);
		case EField(e,f):
			doEncode(e);
			doEncodeString(f);
		case EBinop(op,e1,e2):
			doEncodeString(op);
			doEncode(e1);
			doEncode(e2);
		case EUnop(op,prefix,e):
			doEncodeString(op);
			bout.addByte(prefix?1:0);
			doEncode(e);
		case ECall(e,el):
			doEncode(e);
			bout.addByte(el.length);
			for( e in el )
				doEncode(e);
		case EIf(cond,e1,e2):
			doEncode(cond);
			doEncode(e1);
			if( e2 == null )
				bout.addByte(255);
			else
				doEncode(e2);
		case EWhile(cond,e):
			doEncode(cond);
			doEncode(e);
		case EFor(v,it,e):
			doEncodeString(v);
			doEncode(it);
			doEncode(e);
		case EBreak, EContinue:
		case EFunction(params,e,name,_):
			bout.addByte(params.length);
			for( p in params )
				doEncodeString(p.name);
			doEncode(e);
			doEncodeString(name == null?"":name);
		case EReturn(e):
			if( e == null )
				bout.addByte(255);
			else
				doEncode(e);
		case EArray(e,index):
			doEncode(e);
			doEncode(index);
		case EArrayDecl(el):
			if( el.length >= 255 ) throw "assert";
			bout.addByte(el.length);
			for( e in el )
				doEncode(e);
		case ENew(cl,params):
			doEncodeString(cl);
			bout.addByte(params.length);
			for( e in params )
				doEncode(e);
		case EThrow(e):
			doEncode(e);
		case ETry(e,v,_,ecatch):
			doEncode(e);
			doEncodeString(v);
			doEncode(ecatch);
		case EObject(fl):
			bout.addByte(fl.length);
			for( f in fl ) {
				doEncodeString(f.name);
				doEncode(f.e);
			}
		case ETernary(cond, e1, e2):
			doEncode(cond);
			doEncode(e1);
			doEncode(e2);
		}
	}

	function doDecode() : Expr {
		return switch( bin.get(pin++) ) {
		case 0:
			EConst( doDecodeConst() );
		case 1:
			EIdent( doDecodeString() );
		case 2:
			var v = doDecodeString();
			EVar(v,doDecode());
		case 3:
			EParent(doDecode());
		case 4:
			var a = new Array();
			for( i in 0...bin.get(pin++) )
				a.push(doDecode());
			EBlock(a);
		case 5:
			var e = doDecode();
			EField(e,doDecodeString());
		case 6:
			var op = doDecodeString();
			var e1 = doDecode();
			EBinop(op,e1,doDecode());
		case 7:
			var op = doDecodeString();
			var prefix = bin.get(pin++) != 0;
			EUnop(op,prefix,doDecode());
		case 8:
			var e = doDecode();
			var params = new Array();
			for( i in 0...bin.get(pin++) )
				params.push(doDecode());
			ECall(e,params);
		case 9:
			var cond = doDecode();
			var e1 = doDecode();
			EIf(cond,e1,doDecode());
		case 10:
			var cond = doDecode();
			EWhile(cond,doDecode());
		case 11:
			var v = doDecodeString();
			var it = doDecode();
			EFor(v,it,doDecode());
		case 12:
			EBreak;
		case 13:
			EContinue;
		case 14:
			var params = new Array();
			for( i in 0...bin.get(pin++) )
				params.push({ name : doDecodeString(), t : null });
			var e = doDecode();
			var name = doDecodeString();
			EFunction(params,e,(name == "") ? null: name);
		case 15:
			EReturn(doDecode());
		case 16:
			var e = doDecode();
			EArray(e,doDecode());
		case 17:
			var el = new Array();
			for( i in 0...bin.get(pin++) )
				el.push(doDecode());
			EArrayDecl(el);
		case 18:
			var cl = doDecodeString();
			var el = new Array();
			for( i in 0...bin.get(pin++) )
				el.push(doDecode());
			ENew(cl,el);
		case 19:
			EThrow(doDecode());
		case 20:
			var e = doDecode();
			var v = doDecodeString();
			ETry(e,v,null,doDecode());
		case 21:
			var fl = new Array();
			for( i in 0...bin.get(pin++) ) {
				var name = doDecodeString();
				var e = doDecode();
				fl.push({ name : name, e : e });
			}
			EObject(fl);
		case 255:
			null;
		default:
			throw "Invalid code "+bin.get(pin - 1);
		}
	}

	public static function encode( e : Expr ) : haxe.io.Bytes {
		var b = new Bytes();
		b.doEncode(e);
		return b.bout.getBytes();
	}

	public static function decode( bytes : haxe.io.Bytes ) : Expr {
		var b = new Bytes(bytes);
		return b.doDecode();
	}

}